Skip to content

Never advance the chain past a record that was not written - #41

Merged
repsecure merged 1 commit into
mainfrom
agent/3.0-durable-append
Aug 5, 2026
Merged

Never advance the chain past a record that was not written#41
repsecure merged 1 commit into
mainfrom
agent/3.0-durable-append

Conversation

@repsecure

Copy link
Copy Markdown
Owner

Never advance the chain past a record that was not written.

The defect

Chain state advanced before the sinks ran, so when appendFileSync hit ENOSPC the record never reached disk while the in-memory chain had already moved on. Verification then reported an index gap and a broken link, which is byte-identical to corpus case b3, an attacker deleting a record. A full disk and a deletion were indistinguishable, and nothing was logged.

Reproduced on a 128k tmpfs filled to zero available blocks, confirming appends genuinely stopped landing rather than assuming it.

The fix

The chain advances only after a durable sink accepts the record, so a lost record leaves no index jump and no broken link. Contiguity alone would make the loss invisible, so the loss is then declared: the refused payload goes to stderr, /health carries counters, and the first successful append writes an in-chain gap declaration that both verifiers report as the non-fatal chain-gap-declared.

Sinks are now typed durable or observational. Observers run only after the evidence stream accepts, so a console can never display a record the chain does not contain, and an observer that throws cannot hold the chain back.

Halting the process was rejected: src/index.ts is explicit that neither the chain nor the dashboard may break egress, and halting does not empty a disk. It converts a logging outage into a gating outage.

Two further defects found while fixing it

Short writes fused records. ENOSPC can write part of a line and then fail, leaving a headless fragment with no terminator, and the next append landed on that same line. A full disk therefore destroyed a record that was written on top of the one that was not. The sink now rolls a partial write back to a mark taken at the last completed append, guarded so a truncate can never eat whole records.

A failed console write killed the process. Pre-existing on main.

Also closed

verifyCapabilityTicketSignature was exported, tested, and called from nowhere, and policy precedence had no conflict test. Both addressed.

emit() advanced chainIndex and previousHash before the sinks ran and swallowed
whatever they threw. A record that never reached disk still moved the chain on,
so the next record carried an index jump and a broken link: the shape of a
DELETED record. `agentwall verify` reported a full partition with the same
findings as the corpus forgery b3-record-removed, while the process stayed up
and said nothing.

The chain now advances only after a durable sink accepts the record, so the file
stays contiguous across a loss and neither the index nor the link reads as an
edit. The refused payload goes to stderr under `agentwall_audit_dropped` with no
integrity block, /health carries the counters, and the first append that
succeeds afterwards writes a gap declaration record that both verifiers report
as the non-fatal chain-gap-declared. A declaration never excuses an index gap, a
link break, or a hash mismatch.

Two failures found alongside it. A short write left a fragment with no
terminator that the next append fused onto, so a full disk destroyed a record
that WAS written on top of the one that was not; the sink now rolls its own
partial write back. And a stream redirected to a regular file reports a failed
writeSync as an 'error' event rather than an exception, so the per-sink
try/catch never saw it and an unhandled event killed the process on the next
tick, taking egress gating down with it.

Also: remove capability tickets, which /evaluate minted and nothing ever
presented back, and cover the decision precedence conflict that no test reached.
@repsecure
repsecure force-pushed the agent/3.0-durable-append branch from 185b11e to 8729f26 Compare August 5, 2026 22:31
@repsecure
repsecure merged commit 2bbc39a into main Aug 5, 2026
10 checks passed
@repsecure
repsecure deleted the agent/3.0-durable-append branch August 5, 2026 22:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant